package model;
/**
 * 
 * This class implements methods that handle the gameplay mechanics
 * such as dealing cards, determining and conceding turns, as well
 * as determining the winner and splitting the pot accordingly.
 *
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.io.Serializable;

public class Table implements Serializable{
	//This is just for the console testing:
	Scanner input=new Scanner(System.in);
	
	/**
	 * The status of the table is either IN_GAME, meaning a game is being played
	 * or DONE meaning the game is over
	 */
	public enum TableStatus{
		IN_GAME, DONE;
	}
	private static int numberOfTables = 0;
	
	private int tableID;
	private TableSettings tableSettings;
	private ArrayList<Player> players;
	private HashMap<String, Player> playerHash;
	private int numberOfPlayers;
	private TableStatus status;
	
	private Deck deck;
	private int currentPot;
	private Player currentTurn;
	private int currentBet;
	
	private int callCount;
	private int streetCount;
	private boolean endEarly = false;

	private ArrayList<String> messages;
	
	/**
	 * Constructor set all attributes to default values
	 * @param name table name
	 */
	public Table(String name) {		
		tableID = numberOfTables + 1;
		numberOfTables++;
		tableSettings = new TableSettings(name);
		setStatus(TableStatus.DONE);
		
		players = new ArrayList<Player>();
		playerHash = new HashMap<String, Player>();
		numberOfPlayers = 0;
		deck = new Deck();
		
		callCount = 0;
		streetCount = 0;
		messages = new ArrayList<String>();
	}
	
	/**
	 * Constructor that uses the User-defined table settings to create the table
	 * @param TableSettings User-defined table settings.
	 */
	public Table(TableSettings tableSettings){
		tableID = numberOfTables + 1;
		numberOfTables++;
		this.tableSettings = tableSettings;
		setStatus(TableStatus.DONE);
		
		players = new ArrayList<Player>();
		playerHash = new HashMap<String, Player>();
		numberOfPlayers = 0;
		deck = new Deck();
		callCount = 0;
		streetCount = 0;
		messages = new ArrayList<String>();
	}

	//Getters and setters:
	
	public int getTableID() {
		return tableID;
	}

	public TableSettings getTableSettings() {
		return tableSettings;
	}
	
	public ArrayList<Player> getPlayers() {
		return players;
	}

	public int getCurrentPot() {
		return currentPot;
	}

	public void setCurrentPot(int currentPot) {
		this.currentPot = currentPot;
	}

	public Player getCurrentTurn() {
		return currentTurn;
	}

	public void setCurrentTurn(Player currentTurn) {
		this.currentTurn = currentTurn;
	}

	public int getCurrentBet() {
		return currentBet;
	}

	public void setCurrentBet(int currentBet) {
		this.currentBet = currentBet;
	}

	public int getAnte() {
		return tableSettings.getAnte();
	}

	public int getMaxBet() {
		return tableSettings.getMaxBet();
	}
	
	public TableStatus getStatus() {
		return status;
	}

	public void setStatus(TableStatus status) {
		this.status = status;
	}
	
	public int getCallCount() {
		return callCount;
	}

	public void setCallCount(int callCount) {
		this.callCount = callCount;
	}
	
	public void incrementCallCount(){
		callCount = callCount + 1;
	}

	public int getStreetCount() {
		return streetCount;
	}

	public void setStreetCount(int streetCount) {
		this.streetCount = streetCount;
	}
	
	public void incrementStreetCount(){
		streetCount++;
	}

	public boolean isEndEarly() {
		return endEarly;
	}

	public void setEndEarly(boolean endEarly) {
		this.endEarly = endEarly;
	}

	public int getSlotsAvailable() {
		return tableSettings.getMaxPlayers() - this.numberOfPlayers;
	}

	public void addMessage(String username, String message) {
		messages.add("<" + username + "> " + message);	
	}
	
	public ArrayList<String> getMessages() {
		return messages;
	}
		
	/**
	 * Get the name of the player who is at the specific seat
	 * @param seat integer value of the seat the player to be found
	 * @return the username string of the player at the given seat
	 */
	public String getPlayerName(int seat) {
		if (seat >= players.size()) {
			return null;
		}
		return players.get(seat).getName();
	}

	public void updateTableStatus() {
		if (this.isActive()) {
			// Log!
		}
		else if (this.numberOfPlayers < 2) {
			System.out.println("Waiting for " + (2 - this.numberOfPlayers) + " tablePlayers...");
		}
		else {
			this.setStatus(TableStatus.IN_GAME);
			for(Player player: players){
				player.setActive();
			}
			initialiseHand();
		}
	}
	
	public boolean isActive() {
		return status == TableStatus.IN_GAME;
	}
	
	public boolean isDone(){
		return status == TableStatus.DONE;
	}
	
	/**
	 * Add a player to the table at the end of the round
	 * @param player player to be added to tablePlayers
	 */
	public void addPlayer(Player player){
		while(!this.status.equals(TableStatus.DONE)){
			//wait for current round to end to end
		}
		
		//if play is active, set to inactive (by folding)
		if (player.getActive())
			player.fold();
		
		//add player to table and update number of players
		players.add(player);
		playerHash.put(player.getName(), player);
		numberOfPlayers++;
	}
	
	/**
	 * @param username the username of the wanted player
	 * @return the Player object associated to the given username
	 */
	public Player getPlayer(String username) {
		return playerHash.get(username);
	}
	
	/**
	 * Remove a player from the table at the end of the round
	 * @param player to be removed from players
	 */
	public void removePlayer(Player player) {
		while(!this.status.equals(TableStatus.DONE)){
			//wait for current round to end to end
		}
		
		players.remove(player);		
		playerHash.remove(player.getName());
		player.cashOut();
		numberOfPlayers--;
	}

	/**
	 * @return true if the number of players is at the max, false otherwise
	 */
	public boolean isFull() {
		if (this.numberOfPlayers == tableSettings.getMaxPlayers()) {
			return true;
		}
		else {
			return false;
		}
	}
	
	/**
	 * Adds a card to each active player's hand
	 * @param faceUp if true, deal the cards faceUp, if not, deal them faceDown
	 */
	public void dealFaceUp(boolean faceUp){
		Hand playerHand;
		Card currentCard;
		
		//for each active player on the table add a card from the deck
		for(Player player: players){
			if(player.getActive()){
				playerHand = player.getCurrentHand();
				currentCard = deck.drawCard(); 
				currentCard.setFaceUp(faceUp);
				playerHand.addCard(currentCard);
			}
		}
		
		//TODO remove this for actual implementation, this is just for TXT tests
		for(Player player: players){
			System.out.println("Player " + player.getName() + " was dealt " + player.getCurrentHand());
		}
	}
	
	/**
	 * Increase amount in the pot by a given amount
	 * @param chips amount to increase pot
	 */
	public void increasePot(int chips){
		currentPot += chips;
	}
	
	/**
	 * Increase amount of the current bet by a given amount
	 * @param betValue amount to increase bet
	 */
	public void increaseBet(int betValue){
		currentBet += betValue;
	}
	
	/**
	 * @return the number of active players at the table
	 */
	public int countActivePlayers(){
		int count = 0;
		for(Player player: players){
			if(player.getActive())
				count++;
		}
		return count;
	}
	
	/**
	 * Set the player to inTurn
	 * @param player player whose turn it is
	 */
	public void setTurn(Player player){
		player.setTurn();
		setCurrentTurn(player);
	}
	
	/**
	 * Get the index of the player whose turn it is
	 * @return index of player whose turn it is
	 */
	public int currentTurnIndex(){
		int index=-1;
		for(int i=0; i<players.size(); i++){
			if(players.get(i).inTurn())
				index = i;
		}
		return index;
	}
	
	/**
	 * Set the nextTurn to the next active player after the given index
	 * @param currentIndex the index of the player whose currently in their turn
	 */
	public void setNextTurn(int currentIndex){
		boolean found = false;
		
		for(int i=currentIndex+1; i<players.size() && !found; i++){
			if(players.get(i).getActive()){
				setTurn(players.get(i));
				found = true;
			}
		}
		
		//if we exit the loop, we did not find the player, check first half
		for(int i=0; i<currentIndex && !found; i++){
			if(players.get(i).getActive()){
				setTurn(players.get(i));
				found = true;
			}
		}
		
		if(getCurrentTurn().getChips() == 0){
			setNextTurn();
		}
	}
	
	/**
	 * Set the nextTurn to the next active player at the table
	 * This gets the index of the player whose turn it is and calls on the above method
	 */
	public void setNextTurn(){
		int currentIndex = currentTurnIndex();
		getCurrentTurn().setActive();
		
		setNextTurn(currentIndex);
	}
	
	/**
	 * reset the currentBet of each player
	 */
	public void resetPlayersCurrentBets(){
		for(Player player: players){
			player.resetLastBet();
		}
	}
	
	/**
	 * @return true if only 1 active player remains, false otherwise
	 */
	public boolean allFolded(){
		return countActivePlayers() < 2;
	}
	
	/**
	 * Turns every card of all active players up
	 */
	public void allFaceUp(){
		for(Player player: players){
			if(player.getActive()){
				player.getCurrentHand().setFaceUp();
			}
		}
	}
	
	/**
	 * /**
	 * Determines which player on the table will bet first
	 * depending on the cards in their hand
	 * @param firstRound true if this is the first round of betting, false otherwise
	 * @return player to bet first
	 */
	public Player determineFirstBetter(boolean firstRound) {
		Player first = null;
		
		//if this is the first round of betting, check for the lowest faceUp card
		if(firstRound){
			int tempLowest = 0;
			
			for(Player player: players){
				if (tempLowest == 0) {
					tempLowest = player.getValueOfCard(1);
					first = player;
				}
				else {
					int temp = player.getValueOfCard(1);
					if (temp < tempLowest) {
						tempLowest = temp;
						first = player;
					}
				}
			}
		} 
		/* if this is not the first round of betting, find out who has the best hand
		 * from the visible cards
		 */
		else{
				ArrayList<Player> eligiblePlayers = new ArrayList<Player>();
				
				//get the first active player and create a list of active players in eligible players
				int j=0;
				do{
					eligiblePlayers = bettingList(players.get(j));
					j++;
				} while(j<players.size() && !players.get(j).getActive());
				
				Player currentLeader = eligiblePlayers.get(0);
				int[] leadingHandRank = currentLeader.getCurrentHand().determineVisibleHandRanking();
				int[] currentHandRank = new int[6];
				
				for(int i=1; i<eligiblePlayers.size(); i++){
					currentHandRank = eligiblePlayers.get(i).getCurrentHand().determineVisibleHandRanking();
					//if currentHandRank is greater than leadingHandRank
					if(arrayIsGreater(currentHandRank, leadingHandRank) == 1){
						//set the current leader to the current player
						currentLeader = eligiblePlayers.get(i);
						
						//set the leading hand rank to current hand rank
						leadingHandRank = currentHandRank;
				}
					
				first = currentLeader;
			}
		}
		
		return first;
	}
	
	/**
	 * Resets the pot, bet and deck, activates every player
	 * and resets each player's hand, and deals the first street
	 */
	public void initialiseHand(){	
		//reset pot and bet
		currentPot = 0;
		currentBet = 0;
		
		/* Reset the hand and bets of each player and set each 
		 * INACTIVE player to WAITING_FOR_TURN
		 */
		for(Player player: players){
			player.handReset();
			player.setActive();
			player.resetLastBet();
		}
		
		//reset the deck
		deck = new Deck();
		
		//take ante from each player and reset last bet
		for(Player player: players){
			player.bet(getAnte());
			player.resetLastBet();
		}
		
		//set table to IN_GAME
		setStatus(TableStatus.IN_GAME);
		
		// Deal one card face down, and another card face-up.
		dealFaceUp(false);
		dealFaceUp(true);
	}
		
	/**
	 * This implements the gameplay mechanics of the subsequent streets
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 */
	public void playHand() {	
		//go through first betting round
		bettingRound(true);
		
		//deal next round and get bets 3 more times as long as the game isn't over
		for(int i=0; i<3 && !isDone(); i++){
			dealFaceUp(true);
			bettingRound(false);
		}
		
		//if the game is already over once all cards have been dealt and bets have been taken, end the round
		if(!isDone()){
			endRound(getWinners());
			
			//show the active players faceDown cards
			for(Player player: players){
				if(player.getActive()){
					player.getCurrentHand().setFaceUp();
				}
			}
		}
	}
	
	/**
	 * Determines the first better and gets the bets of each user in order
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @param firstBettingRound if true this is the first betting round
	 */
	public void bettingRound(boolean firstBettingRound){
		Player firstBetter = determineFirstBetter(firstBettingRound);
		
		//as long as the player has chips left
		if(firstBetter.getChips() != 0){
			//make first better bet the bring in
			firstBetter.raise(getAnte()/2);
		}
		
		//generate a betting list (reordered player list)
		ArrayList<Player> bettingList = bettingList(firstBetter);
		
		do{
			//betting for other players
			for(Player player: bettingList){
				//if the player is active and is not the first better
				if(player.getActive() && player.getLastBet() != currentBet){				
					//make player bet
					bet(player);
				}
			}
			
			//set all players who just bet back to WAITING_FOR_TURN
			for(Player player: bettingList){
				if(player.hasBet()){
					player.setActive();
				}
			}
		} while(!allBetEqual());
		
		//reset current bets
		currentBet = 0;
		for(Player player: players){
			player.resetLastBet();
		}
		
	}
	
	/**
	 * Orders the players starting with the first better of the round
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @param firstBetter the better to bet first
	 * @return ordered arrayList of players starting with firstBetter
	 */
	private ArrayList<Player> bettingList(Player firstBetter){
		ArrayList<Player> bettingList = new ArrayList<Player>();

		//get the index of the firstBetter
		int startingPoint = players.lastIndexOf(firstBetter);
		
		//add players starting from firstBetter to the end of players
		for(int i=startingPoint; i<players.size(); i++){
			if(players.get(i).getActive())
				bettingList.add(players.get(i));
		}
		
		//add players starting from 0 to the firstBetter
		for(int i=0; i<startingPoint; i++){
			if(players.get(i).getActive())
				bettingList.add(players.get(i));
		}
		
		return bettingList;
	}
	
	/**
	 * Checks to make sure all players have bet the same amount
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @return true if all players' last bets are equal, false otherwise
	 */
	private boolean allBetEqual(){
		for(Player player: players){
			if(player.getActive() && player.getLastBet() != currentBet)
				return false;
		}
		return true;
	}
	
	/**
	 * Single player goes through betting process
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 */
	public void bet(Player player){
		//set players turn
		player.setTurn();
		
		//if the player has enough chips to bet the current bet, give option to call, raise or fold
		if(currentBet < player.getChips()){
			
			//if player has more chips than the max bet, there is no option to go all in
			if(player.getChips() > getMaxBet()){
				System.out.print(player+" would you like to call (c), raise (r), or fold (f)?\t");
				String decision = input.next();
				decision.toLowerCase();
				
				if(decision.startsWith("f")){
					fold(player);
				}
				else if(decision.startsWith("r")){
					raise(player);
				}
				else if(decision.startsWith("c")){
					call(player);
				}
			}
			
			//if players total chips is less than the max bet, player can also go all in
			else{
				System.out.print(player+" would you like to call (c), raise (r), go all in(a), or fold (f)?\t");
				String decision = input.next();
				decision.toLowerCase();
				
				if(decision.startsWith("f")){
					fold(player);
				}
				else if(decision.startsWith("r")){
					raise(player);
				}
				else if(decision.startsWith("c")){
					call(player);
				}
				else if(decision.startsWith("a")){
					allIn(player);
				}
			}
		}
		/* otherwise currentBet is larger than the players chipcount so player can only go all in or fold
		 * if the player has no more chips, they do not have option to bet
		 */
		else if(player.getChips() != 0){
			System.out.print(player+" would you like to go all in (a) or fold (f)?\t");
			String decision = input.next();
			decision.toLowerCase();
			
			//if player folds
			if(decision.startsWith("f")){
				fold(player);
			}
			else{
				allIn(player);
			}
		}
	}
	
	/**
	 * Helper method to make player call
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @param player player that will call
	 */
	private void call(Player player){
		player.call();
	}
	
	/**
	 * Helper method to make player raise
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @param player player that will raise
	 */
	private void raise(Player player){
		System.out.print("How much would you like to raise? you have "+player.getChips()+" chips to bet: MAX BET="+getMaxBet()+" \t");
		int bet = input.nextInt();
		int totalBet = bet + currentBet;
		
		//make sure the bet is within the bounds
		while(totalBet > player.getChips() || bet > getMaxBet()){
			if(bet > getMaxBet()){
				System.out.print("Please bet less than "+getMaxBet()+"\t");
				bet = input.nextInt();
				totalBet = bet + currentBet;
			}
			if(totalBet > player.getChips()){
				System.out.print("You do not have enough chips, bet less than "+player.getChips()+"\t");
				bet = input.nextInt();
				totalBet = bet + currentBet;
			}
		}
		player.raise(bet);
	}
	
	/**
	 * Helper method to make player go all in
	 * 
	 * This method is used for console testing, however is not used during 
	 * actual gameplay for this is taken care of by the GamePlay remote
	 * 
	 * @param player player that will go all in
	 */
	private void allIn(Player player){
		player.allIn();
	}
	
	/**
	 * Helper method to make player fold
	 * @param player
	 */
	private void fold(Player player){
		player.fold();
		System.out.println(player+" has folded.");

		//if there are less than 2 active players remaining, that player wins
		if(countActivePlayers() < 2){
			endRound(getWinners());
		}
	}
	
	/**
	 * Determines the winner(s) of the hand and the players' game statistics are updated.
	 * @return an ArrayList of the winner(s)
	 */
	public ArrayList<Player> getWinners() {	
		//Initialise an ArrayList for potential winners of the round (in case of draw)
		ArrayList<Player> winners = new ArrayList<Player>();
		ArrayList<Player> eligiblePlayers = new ArrayList<Player>();
		
		//get the first active player and create a list of active players in eligible players
		int j=0;
		do{
			eligiblePlayers = bettingList(players.get(j));
			j++;
		} while(j<players.size() && !players.get(j).getActive());
		
		/* If there are less than 2 eligible players, return the list
		 * There is no need to check the players hands
		 */
		if(eligiblePlayers.size() < 2){
			return eligiblePlayers;
		}
		
		Player currentLeader = eligiblePlayers.get(0);
		int[] leadingHandRank = currentLeader.getCurrentHand().determineHandRanking();
		int[] currentHandRank = new int[6];
		
		//determine the leading player and highest hand ranking
		for(int i=1; i<eligiblePlayers.size(); i++){
			currentHandRank = eligiblePlayers.get(i).getCurrentHand().determineHandRanking();

			//if currentHandRank is greater than leadingHandRank
			if(arrayIsGreater(currentHandRank, leadingHandRank) == 1){
				//set the current leader to the current player
				currentLeader = eligiblePlayers.get(i);
				
				//set the leading hand rank to current hand rank
				leadingHandRank = currentHandRank;
			}
		}
		
		//the leading player out of the loop is one of the winners, add to winners ArrayList
		winners.add(currentLeader);
		
		//check for eligiblePlayers that have the same hand ranking as the leader in case of a tie
		for(Player player: eligiblePlayers){
			//as long as the player is not already the current leader
			if(player.getName() != currentLeader.getName()){
				//if the player has the same hand ranking as the leader add the player to the winners ArrayList
				if(arrayIsGreater(leadingHandRank, player.getCurrentHand().determineHandRanking()) == 0){
					winners.add(player);
				}
				
				//if player is not to be added to winner, update statistics that player lost 
				else{
					player.lostHand();
				}
			}
		}
		
		return winners;
	}
	
	/**
	 * Splits the pot among the winners
	 * @param winners ArrayList of winning players
	 */
	public void endRound(ArrayList<Player> winners){
		//split the winnings among each player in the winnings 
		int winnings = currentPot/winners.size();
		
		/* go through each player in the winners ArrayList
		 * add the winnings to their chip count and update 
		 */
		for(Player winner: winners){
			winner.wonHand(winnings);
			// TODO display winners along with winnings in the GUI
			System.out.println(winner+" has won the round with a " +winner.getCurrentHand().rankingString()+" and gets "+winnings+" chips");
		}
		
		//set the table status to DONE
		status = TableStatus.DONE;
	}
	
	/**
	 * Helper method to determine if array a is greater than b
	 * @param a integer array to be compared
	 * @param b integer array to be compared
	 * @return 1 if a is greater than b, 0 if equal, -1 is a is less than b
	 */
	private int arrayIsGreater(int[] a, int[] b){
		//go through each element of the arrays and compare them
		for(int i=0; i<a.length; i++){
			if(a[i]>b[i]){
				return 1;
			} 
			else if(a[i]<b[i]){
				return -1;
			}
		}
		//if the loop is exited it means the arrays are equal
		return 0;
	}
	
	@Override
	public String toString() {
		String tableString = "Table " + tableSettings.getName() + ": " + this.numberOfPlayers + " currently sitting in.";
		int n = 0;
		for(Player player: players){
			tableString = tableString + "\nPlayer " + (n++) + ": " + player.getName();
		}
		return tableString;
		
	}
	
	public String playersHands(){
		String hands = "Players hands: ";
		for(Player player: players){
			hands = hands + "\n" + player.getName() +
					" \n\t Hand: "+player.getCurrentHand();
		}
		return hands;
	}

}